/**
 * @ignore
 * BEGIN HEADER
 *
 * Contains:        ImageRenderer
 * CVM-Role:        View
 * Maintainer:      Hendrik Erz
 * License:         GNU GPL v3
 *
 * Description:     This renderer can display and manage images.
 *
 * END HEADER
 */

import { renderInlineWidgets } from './base-renderer'
import { type SyntaxNode, type SyntaxNodeRef } from '@lezer/common'
import { WidgetType, type EditorView } from '@codemirror/view'
import { type EditorState } from '@codemirror/state'
import { configField } from '../util/configuration'
import makeValidUri from '@common/util/make-valid-uri'
import { linkImageMenu } from '../context-menu/link-image-menu'
import { trans } from '@common/i18n-renderer'
import clickAndSelect from './click-and-select'

const path = window.path

// This variable holds a base64 encoded placeholder image.
const img404 = ''

class ImageWidget extends WidgetType {
  constructor (readonly node: SyntaxNode, readonly imageTitle: string, readonly imageUrl: string, readonly altText: string, readonly data: string) {
    super()
  }

  eq (other: ImageWidget): boolean {
    return other.imageTitle === this.imageTitle && other.imageUrl === this.imageUrl
  }

  toDOM (view: EditorView): HTMLElement {
    const img = document.createElement('img')
    img.alt = this.altText

    const decodedUrl = this.imageUrl

    let isDataUrl = /^data:[a-zA-Z0-9/;=]+(?:;base64){0,1},.+/.test(decodedUrl)
    let actualURLToLoad = decodedUrl
    const basePath = path.dirname(view.state.field(configField).metadata.path)

    if (!isDataUrl) {
      actualURLToLoad = makeValidUri(actualURLToLoad, basePath)
    }

    img.src = actualURLToLoad

    const caption = document.createElement('figcaption')
    caption.textContent = this.imageTitle
    caption.contentEditable = 'true'

    const size = document.createElement('span')
    size.classList.add('image-size-info')

    const openExternally = document.createElement('span')
    openExternally.classList.add('open-externally-button')
    openExternally.setAttribute('title', trans('Open image externally'))
    openExternally.onclick = function (event) {
      event.stopPropagation()
      // NOTE: We can only do this because the main process prevents any
      // navigation, and will open the "URL" using the shell.
      window.location.assign(actualURLToLoad)
    }

    const openIcon = document.createElement('cds-icon')
    openIcon.setAttribute('shape', 'pop-out')
    openExternally.appendChild(openIcon)

    const figure = document.createElement('figure')
    figure.appendChild(img)
    figure.appendChild(caption)
    figure.appendChild(size)

    const container = document.createElement('div')
    container.classList.add('editor-image-container')
    container.appendChild(figure)

    // Retrieve the size constraints
    const { imagePreviewHeight, imagePreviewWidth } = view.state.field(configField)
    const width = (!Number.isNaN(imagePreviewWidth)) ? `${imagePreviewWidth}%` : '100%'
    const height = (!Number.isNaN(imagePreviewHeight) && imagePreviewHeight < 100) ? `${imagePreviewHeight}vh` : ''

    // Apply the constraints
    img.style.maxWidth = width
    img.style.maxHeight = height

    // Display a replacement image in case the correct one is not found
    img.onerror = () => {
      img.src = img404
      isDataUrl = true
      caption.textContent = trans('Image not found: %s', decodedUrl)
      caption.contentEditable = 'false'
    }

    // Update the image title on load to retrieve the real image size.
    img.onload = () => {
      img.title = `${this.imageTitle} (${img.naturalWidth}x${img.naturalHeight}px)`
      size.innerHTML = `${img.naturalWidth}&times;${img.naturalHeight}`

      if (!isDataUrl) {
        figure.appendChild(openExternally)
      }
    }

    const { from, to } = this.node
    const data = this.data

    // Define a quick inline function that takes care of applying a new caption
    const updateCaptionFunction = function (event: KeyboardEvent|FocusEvent): void {
      if (event instanceof KeyboardEvent && event.key !== 'Enter') {
        // If this is a KeyboardEvent, only perform the action on Enter
        return
      }

      event.preventDefault()
      event.stopPropagation()
      // Make sure there are no quotes since these will break the image
      const newCaption = caption.textContent?.replace(/"/g, '') ?? ''
      // "Why are you setting the caption both as the image description and title?"
      // Well, since all exports sometimes us this, sometimes the other value.
      const newImageTag = `![${newCaption}](${decodedUrl} "${newCaption}")${data}`
      // Remove the event listeners beforehand to prevent multiple dispatches
      caption.removeEventListener('keydown', updateCaptionFunction)
      caption.removeEventListener('focusout', updateCaptionFunction)
      view.dispatch({ changes: { from, to, insert: newImageTag } })
    }

    // Should work on these events
    caption.addEventListener('keydown', updateCaptionFunction)
    caption.addEventListener('focusout', updateCaptionFunction)

    container.addEventListener('contextmenu', (event) => {
      event.preventDefault()
      event.stopPropagation()
      linkImageMenu(view, this.node, { x: event.clientX, y: event.clientY })
    })

    container.addEventListener('click', event => {
      if (event.target === img) {
        clickAndSelect(view)(event)
      }
    })

    return container
  }

  ignoreEvent (event: Event): boolean { return true }
}

function shouldHandleNode (node: SyntaxNodeRef): boolean {
  return node.type.name === 'Image'
}

function createWidget (state: EditorState, node: SyntaxNodeRef): ImageWidget|undefined {
  // Get the actual link contents, extract title and URL and create a
  // replacement widget
  const literalImage = state.sliceDoc(node.from, node.to)
  const match = /(?<=\s|^)!\[(.*?)\]\((.+?(?:(?<= )"(.+)")?)\)({[^{]+})?/.exec(literalImage)
  if (match === null) {
    return undefined // Should not happen, but we never know.
  }

  // The image RE will give us the following groups:
  // p1: The alternative text (in square brackets)
  // p2: The complete contents of the round braces
  // p3: If applicable, an image title (within round braces)
  // p4: Anything in curly brackets (mostly commands for Pandoc)
  let altText = match[1] ?? '' // Everything inside the square brackets
  let url = match[2] ?? '' // The URL
  let title = match[3] ?? altText // An optional title in quotes after the image
  let p4 = match[4] ?? ''

  // Remove the "title" from the surrounding URL group, if applicable.
  if (match[3] !== undefined) {
    url = url.replace(`"${match[3]}"`, '').trim()
  }

  return new ImageWidget(node.node, title, url, altText, p4)
}

export const renderImages = renderInlineWidgets(shouldHandleNode, createWidget)
